{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The [previous notebook](2-Pipeline.ipynb) showed all the steps required to get a datashader rendering of your dataset, yielding raster images displayed using [Jupyter](http://jupyter.org)'s \"rich display\" support.  However, these bare images do not show the data ranges or axis labels, making them difficult to interpret.  Moreover, they are only static images, and datasets often need to be explored at multiple scales, which is much easier to do in an interactive program.  \n",
    "\n",
    "To get axes and interactivity, the images generated by datashader need to be embedded into a plot using an external library like [Matplotlib](http://matplotlib.org) or [Bokeh](http://bokeh.pydata.org). As we illustrate below, the most convenient way to make datashader plots using these libraries is via the [HoloViews](http://holoviews.org) high-level data-science API.  Datashader itself also provides some limited support for use with Bokeh natively, and native datashader support for Matplotlib has been [sketched](https://github.com/bokeh/datashader/pull/200) but is not yet released.  \n",
    "\n",
    "In this notebook, we will first look at datashader's native Bokeh support, because it uses the same API introduced in the previous examples.  We'll start with the same example from the [previous notebook](2-Pipeline.ipynb):"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import pandas as pd\n",
    "import numpy as np\n",
    "import datashader as ds\n",
    "import datashader.transfer_functions as tf\n",
    "from collections import OrderedDict as odict\n",
    "\n",
    "num=100000\n",
    "np.random.seed(1)\n",
    "\n",
    "dists = {cat: pd.DataFrame(odict([('x',np.random.normal(x,s,num)), \n",
    "                                  ('y',np.random.normal(y,s,num)), \n",
    "                                  ('val',val), \n",
    "                                  ('cat',cat)]))      \n",
    "         for x,  y,  s,  val, cat in \n",
    "         [(  2,  2, 0.03, 10, \"d1\"), \n",
    "          (  2, -2, 0.10, 20, \"d2\"), \n",
    "          ( -2, -2, 0.50, 30, \"d3\"), \n",
    "          ( -2,  2, 1.00, 40, \"d4\"), \n",
    "          (  0,  0, 3.00, 50, \"d5\")] }\n",
    "\n",
    "df = pd.concat(dists,ignore_index=True)\n",
    "df[\"cat\"]=df[\"cat\"].astype(\"category\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Bokeh provides interactive plotting in a web browser. To make an interactive datashader plot when working with Bokeh directly, we'll first need to write a \"callback\" that wraps up the plotting steps shown in the previous notebook.  A callback is a function that will render an image of the dataframe above when given some parameters:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def image_callback(x_range, y_range, w, h, name=None):\n",
    "    cvs = ds.Canvas(plot_width=w, plot_height=h, x_range=x_range, y_range=y_range)\n",
    "    agg = cvs.points(df, 'x', 'y', ds.count_cat('cat'))\n",
    "    img = tf.shade(agg)\n",
    "    return tf.dynspread(img, threshold=0.50, name=name)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "As you can see, this callback is a function that lets us generate a Datashader image covering any range of data space that we want to examine:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "tf.Images(image_callback(None,        None,      300, 300, name=\"Original\"),\n",
    "          image_callback((  0, 4  ), (  0, 4  ), 300, 300, name=\"Zoom 1\"),\n",
    "          image_callback((1.9, 2.1), (1.9, 2.1), 300, 300, name=\"Zoom 2\"))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "You can now see that the single apparent \"red dot\" from the original image is actually a large collection of overlapping points (100,000, to be exact).  However, you can also see that it would be awkward to explore a dataset using static images in this way, having to guess at numerical ranges as in the code above.  Instead, let's make an interactive Bokeh plot using a convenience utility from Datashader called ``InteractiveImage``:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from datashader.bokeh_ext import InteractiveImage\n",
    "import bokeh.plotting as bp\n",
    "\n",
    "bp.output_notebook()\n",
    "p = bp.figure(tools='pan,wheel_zoom,reset', x_range=(-5,5), y_range=(-5,5), plot_width=500, plot_height=500)\n",
    "\n",
    "InteractiveImage(p, image_callback)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "``InteractiveImage`` accepts any Bokeh figure and a callback that returns an image when given the range and pixel size.  Now we can see the full axes corresponding to this data, and we can also zoom in using a scroll wheel (as long as the \"wheel zoom\" tool is enabled on the right) or pan by clicking and dragging (as long as the \"pan\" tool is enabled on the right).  Each time you zoom or pan, the callback will be given the new viewport that's now visible, and datashader will render a new image to update the display. The result makes it look as if all of the data is available in the web browser interactively, while only ever storing a single image at any one time. In this way, full interactivity can be provided even for data that is far too large to display in a web browser directly. (Most web browsers can handle tens of thousands or hundreds of thousands of data points, but not millions or billions!)\n",
    "\n",
    "***Note that you'll only see an updated image on zooming in if there is a live Python process running.*** Bokeh works by taking a Python specification for a plot and generating a corresponding JavaScript-based visualization in the browser.  Whatever data has been given to the browser can be viewed interactively, but in this case only a single image of the data is given at a time, and so you will not be able to see more detail when zooming in unless the Python (and thus Datashader) process is running.  In a static HTML export of this notebook, such as those on a website, you'll only see the original pixels getting larger, not a zoomed-in rendering as in the callback plots above.\n",
    "\n",
    "``InteractiveImage`` lets you explore any Datashader pipeline you like, but unfortunately it only works in a Jupyter notebook (not a deployed Bokeh server), and it is not typically possible to combine such a plot with other Bokeh figures.  The [dashboard.py](https://github.com/bokeh/datashader/blob/cb2f49f9/examples/dashboard/dashboard.py) from datashader 0.6 gives an example of building Bokeh+Datashader visualizations from the ground up, but this approach is quite difficult and is not recommended for most users. For these reasons, we do not recommend using InteractiveImage in new projects.  Luckily, a much more practical approach to embedding and interactivity is available using HoloViews, as shown in the rest of this guide."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Embedding Datashader with HoloViews\n",
    "\n",
    "[HoloViews](http://holoviews.org) (1.7 and later) is a high-level data analysis and visualization library that makes it simple to generate interactive [Datashader](https://github.com/bokeh/datashader)-based plots.  Here's an illustration of how this all fits together when using HoloViews+[Bokeh](http://bokeh.pydata.org):\n",
    "\n",
    "  ![Datashader+Holoviews+Bokeh](../assets/images/ds_hv_bokeh.png)\n",
    "\n",
    "HoloViews offers a data-centered approach for analysis, where the same tool can be used with small data (anything that fits in a web browser's memory, which can be visualized with Bokeh directly), and large data (which is first sent through Datashader to make it tractable) and with several different plotting frontends.  A developer willing to do more programming can do all the same things separately, using Bokeh, Matplotlib, and Datashader's APIs directly, but with HoloViews it is much simpler to explore and analyze data. Of course, the [previous notebook](1-Pipeline.ipynb) showed that you can also use datashader without either any plotting library at all (the light gray pathways above), but then you wouldn't have interactivity, axes, and so on.\n",
    "\n",
    "Most of this notebook will focus on HoloViews+Bokeh to support full interactive plots in web browsers, but we will also briefly illustrate the non-interactive HoloViews+Matplotlib approach.  Let's start by importing some parts of HoloViews and setting some defaults:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import holoviews as hv\n",
    "import holoviews.operation.datashader as hd\n",
    "hd.shade.cmap=[\"lightblue\", \"darkblue\"]\n",
    "hv.extension(\"bokeh\", \"matplotlib\") "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### HoloViews+Bokeh\n",
    "\n",
    "Rather than starting out by specifying a figure or plot, in HoloViews you specify an [``Element``](http://holoviews.org/reference/index.html#elements) object to contain your data, such as `Points` for a collection of 2D x,y points. To start, let's define a Points object wrapping around a small dataframe with 10,000 random samples from the ``df`` above:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "points = hv.Points(df.sample(10000))\n",
    "\n",
    "points"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "As you can see, the ``points`` object visualizes itself as a Bokeh plot, where you can already see many of the [problems that motivate datashader](https://anaconda.org/jbednar/plotting_pitfalls) (overplotting of points, being unable to detect the closely spaced dense collections of points shown in red above, and so on). But this visualization is just the default representation of ``points``, using Jupyter's [rich display](https://anaconda.org/jbednar/rich_display) support; the actual ``points`` object itself is merely a data container:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "points.data.head()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### HoloViews+Datashader+Matplotlib\n",
    "\n",
    "The default visualizations in HoloViews work well for small datasets, but larger ones will have overplotting issues as are already visible above, and will eventually either overwhelm the web browser (for the Bokeh frontend) or take many minutes to plot (for the Matplotlib backend). Luckily, HoloViews provides support for using Datashader to handle both of these problems:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%output backend=\"matplotlib\"\n",
    "\n",
    "agg = ds.Canvas().points(df,'x','y')\n",
    "\n",
    "hd.datashade(points)  +  hd.shade(hv.Image(agg))  +  hv.RGB(np.array(tf.shade(agg).to_pil()))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Here we asked HoloViews to plot ``df`` using Datashader+Matplotlib, in three different ways:\n",
    "- **A**: HoloViews aggregates and shades an image directly from the ``points`` object using its own datashader support, then passes the image to Matplotlib to embed into an appropriate set of axes.\n",
    "- **B**: HoloViews accepts a pre-computed datashader aggregate, reads out the metadata about the plot ranges that is stored in the aggregate array, and passes it to Matplotlib for colormapping and then embedding.\n",
    "- **C**: HoloViews accepts a PIL image computed beforehand and passes it to Matplotlib for embedding.  \n",
    "\n",
    "As you can see, option A is the most convenient; you can simply wrap your HoloViews element with ``datashade`` and the rest will be taken care of.  But if you want to have more control by computing the aggregate or the full RGB image yourself using the API from the [previous notebook](2-Pipeline.ipynb) you are welcome to do so while using HoloViews+Matplotlib (or HoloViews+Bokeh, below) to embed the result into labelled axes.\n",
    "\n",
    "\n",
    "### HoloViews+Datashader+Bokeh\n",
    "\n",
    "The Matplotlib interface only produces a static plot, i.e., a PNG or SVG image, but the [Bokeh](http://bokeh.pydata.org) interface of HoloViews adds the dynamic zooming and panning necessary to understand datasets across scales:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "hd.datashade(points)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Here, ``hd.datashade`` is not just a function call; it is an \"operation\" that dynamically calls datashader every time a new plot is needed by Bokeh, without the need for any explicit callback functions.  The above plot will automatically be interactive when using the Bokeh frontend to HoloViews, and datashader will be called on each zoom or pan event if you have a live Python process running.  \n",
    "\n",
    "The powerful feature of operations is that you can chain them to make expressions for complex interactive visualizations.  For instance, here is a Bokeh plot that works like the one created by ``InteractiveImage`` at the start of this notebook:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "datashaded = hd.datashade(points, aggregator=ds.count_cat('cat')).redim.range(x=(-5,5),y=(-5,5))\n",
    "hd.dynspread(datashaded, threshold=0.50, how='over').opts(plot=dict(height=500,width=500))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Compared to using ``InteractiveImage``, the HoloViews approach is simpler for the most basic plots (e.g. ``hd.datashade(hv.Points(df))``) while allowing plots to be overlaid and laid out together very flexibly. You can read more about HoloViews support for Datashader at [holoviews.org](http://holoviews.org/user_guide/Large_Data.html)."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### HoloViews+Datashader+Bokeh Legends\n",
    "\n",
    "Because the underlying plotting library only ever sees an image when using Datashader, providing legends and keys has to be handled separately from any underlying support for those features in the plotting library. We are working to simplify this process, but for now you can show a categorical legend by adding a suitable collection of labeled dummy points:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from datashader.colors import Sets1to3\n",
    "\n",
    "datashaded  = hd.datashade(points, aggregator=ds.count_cat('cat'), color_key=Sets1to3)\n",
    "gaussspread = hd.dynspread(datashaded, threshold=0.50, how='over').opts(plot=dict(height=400,width=400))\n",
    "\n",
    "color_key = [(name,color) for name,color in zip([\"d1\",\"d2\",\"d3\",\"d4\",\"d5\"], Sets1to3)]\n",
    "color_points = hv.NdOverlay({n: hv.Points([0,0], label=str(n)).opts(style=dict(color=c)) for n,c in color_key})\n",
    "\n",
    "color_points * gaussspread"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### HoloViews+Datashader+Bokeh Hover info\n",
    "\n",
    "As you can see, converting the data to an image using Datashader makes it feasible to work with even very large datasets interactively.  One unfortunate side effect is that the original datapoints and line segments can no longer be used to support \"tooltips\" or \"hover\" information directly; that data simply is not present at the browser level, and so the browser cannot unambiguously report information about any specific datapoint. Luckily, you can still provide hover information that reports properties of a subset of the data in a separate layer, or you can provide information for a spatial region of the plot rather than for specific datapoints.  For instance, in some small rectangle you can provide statistics such as the mean, count, standard deviation, etc.  E.g. here let's calculate the count for each small square region:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%opts QuadMesh [tools=['hover']] (alpha=0 hover_alpha=0.2)\n",
    "from holoviews.streams import RangeXY\n",
    "\n",
    "pts = hd.datashade(points, width=400, height=400)\n",
    "\n",
    "(pts * hv.QuadMesh(hd.aggregate(points, width=10, height=10, dynamic=False))).relabel(\"Fixed hover\") + \\\n",
    "\\\n",
    "(pts * hv.util.Dynamic(hd.aggregate(points, width=10, height=10, streams=[RangeXY]), \n",
    "                               operation=hv.QuadMesh)).relabel(\"Dynamic hover\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "In the above examples, the plot on the left provides hover information at a fixed spatial scale, while the one on the right reports on an area that scales with the zoom level so that arbitrarily small regions of data space can be examined, which is generally more useful.\n",
    "\n",
    "As you can see, HoloViews makes it just about as simple to work with Datashader-based plots as regular Bokeh plots (at least if you don't need hover or color keys!), letting you visualize data of any size interactively in a browser using just a few lines of code. Because Datashader-based HoloViews plots are just one or two extra steps added on to regular HoloViews plots, they support all of the same features as regular HoloViews objects, and can freely be laid out, overlaid, and nested together with them.  See [holoviews.org](http://holoviews.org) for examples and documentation for how to control the appearance of these plots and how to work with them in general."
   ]
  }
 ],
 "metadata": {
  "language_info": {
   "name": "python",
   "pygments_lexer": "ipython3"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 1
}
